package searcher;

import de.schlichtherle.io.File;

/**
 * Searcher is the main class in the Searcher project. It contains the main method to run the
 * Searcher program from the command line. It also contains much of the logic used in searching.
 * 
 * @version $Id: Searcher.java,v 1.13 2007/09/05 19:50:01 abutler Exp $
 */
public class Searcher
{
	private java.io.PrintStream thePS;

	private int theMaxZipLevel;

	private java.util.Comparator<java.io.File> theFileCompare;

	private java.util.List<FileMatch> theResults;

	private volatile boolean stop;

	private volatile boolean pause;

	/**
	 * Creates a Searcher
	 */
	public Searcher()
	{
		thePS = System.out;
		theMaxZipLevel = -1;
		theFileCompare = new FileComparator();
	}

	private void checkStop()
	{
		if(stop)
			throwStop();
		while(pause)
		{
			try
			{
				Thread.sleep(100);
			} catch(InterruptedException e)
			{
				System.err.println("Searcher pause interrupted");
			}
			if(stop)
				throwStop();
		}
	}

	private void throwStop()
	{
		FileMatch [] matches;
		if(theResults == null)
			matches = null;
		else
			matches = theResults.toArray(new FileMatch [0]);
		throw new StopException(matches);
	}

	/**
	 * @return The maximum number of levels deep this Searcher will search. For example, if the
	 *         level is 2, then the contents of a zip file will be searched, and if that zip file
	 *         contains a zip file, the sub file will be searched, but if the sub file contains
	 *         another zip file, it will not be searched.
	 */
	public int getZipLevel()
	{
		return theMaxZipLevel;
	}

	/**
	 * Sets the zip level for this searcher
	 * 
	 * @param zl The zip level for this searcher
	 * @see #getZipLevel()
	 */
	public void setZipLevel(int zl)
	{
		theMaxZipLevel = zl;
	}

	/**
	 * Performs the search on the given file with the given search criteria
	 * 
	 * @param aFile The file to search recursively
	 * @param include The inclusion criteria that determines when a match is found and reported
	 * @param exclude The set of exclusion criteria--files that match any of these patterns or their
	 *        children will be searched
	 * @param verbose Whether to print every file searched.
	 * @return The file matches that were found during the search
	 */
	public FileMatch [] search(File aFile, SearchCriteria include,
		java.util.List<SearchCriteria> exclude, boolean verbose)
	{
		if(theResults != null)
		{
			synchronized(this)
			{
				if(theResults != null)
					throw new IllegalStateException("A search is already being"
						+ " conducted on this searcher.  The current search"
						+ " must finish or be stopped before another search" + " can begin");
			}
		}
		theResults = new java.util.ArrayList<FileMatch>();
		try
		{
			addMatches(aFile, include, exclude, theResults, verbose);
		} finally
		{
			try
			{
				File.umount();
			} catch(de.schlichtherle.io.ArchiveException e)
			{
				e.printStackTrace();
				System.err.println("Could not unmount all zip files");
			}
		}
		if(stop)
		{
			thePS.println("Search stopped");
			stop = false;
		}
		try
		{
			return theResults.toArray(new FileMatch [0]);
		} finally
		{
			theResults = null;
		}
	}

	private void addMatches(File aFile, SearchCriteria include,
		java.util.List<SearchCriteria> exclude, java.util.List<FileMatch> aList, boolean verbose)
	{
		int i;
		int zipLevel = -1;
		if(theMaxZipLevel >= 0)
		{
			zipLevel = 0;
			File zf = aFile.getEnclArchive();
			while(zf != null)
			{
				checkStop();
				zipLevel++;
				zf = zf.getEnclArchive();
			}
			if(zipLevel > theMaxZipLevel)
				return;
		}
		SearchCriteria xc_i;
		for(i = 0; i < exclude.size(); i++)
		{
			checkStop();
			xc_i = exclude.get(i);
			if(xc_i.matches(aFile) != null)
			{
				if(verbose)
					thePS.println("Excluded file " + aFile + " on pattern "
						+ xc_i.getPathPattern().pattern());
				return;
			}
		}
		if(verbose)
			thePS.println("Searching file " + aFile);
		FileMatch fm = include.matches(aFile);
		if(fm != null)
			aList.add(fm);
		if(!aFile.isArchive() || (theMaxZipLevel < 0 || zipLevel < theMaxZipLevel))
		{
			java.io.File[] subFiles = aFile.listFiles();
			if(subFiles != null)
			{
				java.util.Arrays.sort(subFiles, theFileCompare);
				for(i = 0; i < subFiles.length; i++)
				{
					checkStop();
					addMatches((File) subFiles[i], include, exclude, aList, verbose);
				}
			}
		}
	}

	/**
	 * Causes a search being performed on this Searcher to end
	 */
	public void stop()
	{
		stop = true;
	}

	/**
	 * Undoes a stop operation if it has not taken effect yet
	 */
	public void unstop()
	{
		stop = false;
	}

	/**
	 * Pauses a search
	 */
	public void pause()
	{
		pause = true;
	}

	/**
	 * Resumes a paused search
	 */
	public void unpause()
	{
		pause = false;
	}

	/**
	 * @param ps The PrintStream that this Searcher is to use for output
	 */
	public void setPrintStream(java.io.PrintStream ps)
	{
		thePS = ps;
	}

	/**
	 * Parses a set of command line arguments and performs a search tailored to the user's
	 * specifications
	 * 
	 * @param args The command line arguments to parse
	 * @return The results of the search
	 */
	public FileMatch [] searchWithArgs(String [] args)
	{
		int idx = 0;
		boolean pathCI = false, textCI = false;
		String dir = null, pathPattern = null, textPattern = null;
		java.util.ArrayList<SearchCriteria> exPatterns = new java.util.ArrayList<SearchCriteria>();
		int textBuffer = -1;
		int zipLevel = -1;
		boolean nolines = false, verbose = false;
		while(idx < args.length)
		{
			if(args[idx].equals("-v"))
			{
				verbose = true;
				idx++;
			}
			else if(args[idx].equals("-d"))
			{
				if(idx == args.length - 1)
					throw new IllegalArgumentException("Directory name" + " expected after "
						+ args[idx]);
				if(dir != null)
					throw new IllegalArgumentException("Only one -d argument" + " allowed");
				dir = args[idx + 1];
				idx += 2;
			}
			else if(args[idx].equals("-x") || args[idx].equals("-xi"))
			{
				if(idx == args.length - 1)
					throw new IllegalArgumentException("File pattern" + " expected after "
						+ args[idx]);
				exPatterns.add(new SearchCriteria(args[idx + 1], args[idx].equals("-xi"), null,
					false));
				idx += 2;
			}
			else if(args[idx].equals("-t") || args[idx].equals("-ti"))
			{
				if(idx == args.length - 1)
					throw new IllegalArgumentException("Text pattern" + " expected after "
						+ args[idx]);
				if(textPattern != null)
					throw new IllegalArgumentException("Only one -t[i]" + " argument allowed");
				textPattern = args[idx + 1];
				textCI = args[idx].equals("-ti");
				idx += 2;
			}
			else if(args[idx].equals("-i"))
			{
				if(pathCI)
					throw new IllegalArgumentException("Only one -i argument" + " allowed");
				pathCI = true;
				idx++;
			}
			else if(args[idx].equals("-tb"))
			{
				if(idx == args.length - 1)
					throw new IllegalArgumentException("Integer expected after" + " -tb");
				if(textBuffer >= 0)
					throw new IllegalArgumentException("Only one -tb argument" + " allowed");
				try
				{
					textBuffer = Integer.parseInt(args[idx + 1]);
				} catch(NumberFormatException e)
				{
					throw new IllegalArgumentException("Integer expected after" + " -tb");
				}
				if(textBuffer < 2)
					throw new IllegalArgumentException("Text buffer is too" + " small");
				if(textBuffer > 0x40000000) // 2^30 =1GB
					throw new IllegalArgumentException("Text buffer is too" + " large");
				idx += 2;
			}
			else if(args[idx].equals("-ziplevel"))
			{
				if(idx == args.length - 1)
					throw new IllegalArgumentException("Integer expected after" + " -ziplevel");
				if(zipLevel >= 0)
					throw new IllegalArgumentException("Only one -ziplevel" + " argument allowed");
				try
				{
					zipLevel = Integer.parseInt(args[idx + 1]);
				} catch(NumberFormatException e)
				{
					throw new IllegalArgumentException("Integer expected after" + " -ziplevel");
				}
				if(zipLevel < 0)
					throw new IllegalArgumentException("Zip level must be" + " specified >= 0");
				idx += 2;
			}
			else if(args[idx].equals("-nolines"))
			{
				nolines = true;
				idx++;
			}
			else
			{
				if(pathPattern != null)
					throw new IllegalArgumentException("Only one path pattern" + " is allowed");
				pathPattern = args[idx];
				idx++;
			}
		}
		if(dir == null)
			dir = ".";
		if(textBuffer < 0)
			textBuffer = 0x2000; // 8KB

		setZipLevel(zipLevel);
		SearchCriteria srch = new SearchCriteria(pathPattern, pathCI, textPattern, textCI);
		srch.setTextSearchBufferSize(textBuffer);
		srch.setTextSearchMultiple(textPattern != null && !nolines);
		srch.addSearchListener(new PrettySearchOutput(thePS, !srch.getTextSearchMultiple()));
		return search(new File(dir), srch, exPatterns, verbose);
	}

	/**
	 * The command-line version of Searcher
	 * 
	 * @param args The command line arguments. The form is:<br />
	 *        <code>[-d dir] [path-pattern] [-i] [-x[i] ex-pattern]*
	 *        [-t[i] text-pattern] [-tb XXXX] [-ziplevel XXXX] [-nolines] [-v]
	 *        </code><br />
	 *        Where <code>dir</code> is the directory to search,<br />
	 *        path-pattern is the regular expression to match against a file path,<br />
	 *        the -i option causes the path-pattern to be evaluated case-insensitively,<br />
	 *        the -x option(s) specify patterns for paths to be excluded from the search (i
	 *        suboption is for case-insensitive),<br />
	 *        text-pattern is the regular expression to search a file for (i suboption for
	 *        case-insensitive),<br />
	 *        the -tb option specifies a buffer size for the file search,<br />
	 *        the -ziplevel option specifies how deep to dive into zip files,<br />
	 *        the -nolines option causes a text search to just print the file that a match was found
	 *        in,<br />
	 *        and the -v option specifies verbose.<br />
	 *        All arguments are optional.
	 */
	public static void main(String [] args)
	{
		File.setDefaultArchiveDetector(de.schlichtherle.io.DefaultArchiveDetector.ALL);
		new Searcher().searchWithArgs(args);
	}

	/**
	 * A SearchListener that prints the results of a search to a PrintStream in real time
	 * 
	 * @version $Id: Searcher.java,v 1.13 2007/09/05 19:50:01 abutler Exp $
	 */
	public class PrettySearchOutput implements SearchListener
	{
		/**
		 * The PrintStream to print results to
		 */
		protected java.io.PrintStream out;

		/**
		 * True if this listener only prints when the file has finished being searched, false if
		 * output is printed when the file path matches and on each text match
		 */
		protected boolean justFile;

		private boolean printFileName;

		/**
		 * Creates a PrettySearchOutput
		 * 
		 * @param ps The PrintStream to print search results to
		 * @param justWholeFile Whether this listener prints only the 1-line result or the path
		 *        match and each text match for a file
		 */
		public PrettySearchOutput(java.io.PrintStream ps, boolean justWholeFile)
		{
			out = ps;
			justFile = justWholeFile;
		}

		public void fileSearched(SearchCriteria sc, File aFile)
		{
		}

		public void filePathMatch(SearchCriteria sc, File aFile)
		{
			if(justFile)
				return;
			printFileName = true;
		}

		public void fileTextMatch(SearchCriteria sc, File aFile, TextMatch match)
		{
			if(justFile)
				return;
			if(printFileName)
				out.println(aFile.getPath());
			printFileName = false;
			out.println("\tLine " + match.getLineNumber() + ", char " + match.getCharNumber());
		}

		public void fileMatch(SearchCriteria sc, FileMatch aMatch)
		{
			if(!justFile)
				return;
			out.println(aMatch.getFile());
			if(aMatch.getTextMatches().length > 0)
				out.println(" (" + aMatch.getTextMatches().length + ")");
		}
	}

	/**
	 * This Comparator orders directories last, then does a case-insensitive comparison on the file
	 * name (not the path--it is intended to be used on two files/directories under the same
	 * directory).
	 * 
	 * @version $Id: Searcher.java,v 1.13 2007/09/05 19:50:01 abutler Exp $
	 */
	public class FileComparator implements java.util.Comparator<java.io.File>
	{
		public int compare(java.io.File f1, java.io.File f2)
		{
			if(f1.isDirectory())
			{
				if(!f2.isDirectory())
					return 1;
			}
			else if(f2.isDirectory())
				return -1;
			return f1.getName().compareToIgnoreCase(f2.getName());
		}
	}
}
